package com.wontube;


import java.applet.Applet;
import java.awt.Desktop;
import java.awt.FileDialog;
import java.awt.Frame;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.wontube.media.IObjectListener;


import netscape.javascript.JSObject;

public class Convert2Mp3Applet extends Applet {
	private static final long serialVersionUID = 1L;
	
	private static final String APPCHARSET = "utf-8";
	private static final String VideoType_Mobile = "Mobile";
	private static final String VideoType_Flash = "Flash";
	private static final String VideoType_MP4 = "MP4";
	private static final String VideoType_WebM = "WebM";
	private static final String VideoType_Unknown = "Unknown";

	public Convert2Mp3Applet() {
	}

	@Override
	public void init() {	
		super.init();
		
		final JSObject js = JSObject.getWindow(this);
		
		String dir = super.getParameter("path");

		if (dir != null) {
	        File file = new File(dir);
	        try {
				Desktop.getDesktop().open(file);
			} catch (IOException e) {
			}
	        return;
		}
		
		this.showMsg(js, "Getting video info...");
		
		String url = super.getParameter("url");
		ArrayList<HashMap<String, String>> listResult = this.parse(url);
		final String format = super.getParameter("format");
		HashMap<String, String> mapFile = null;
		if ("mp3".equals(format)) {
			mapFile = this.getVideoEmbedMP3(listResult);
		} else {
			mapFile = this.getVideoEmbedAAC(listResult);
		}
		
		if (mapFile == null) {
			String msg = "No " + format + " found!";
			this.showMsg(js, msg);
			this.resetProcessing(js);
			return;
		}
		
		String videoUrl = mapFile.get("url");
		String title = mapFile.get("title");
		
		FileDialog fd = new FileDialog(new Frame(), "Save As...", 1);
	
		fd.setFilenameFilter(new FilenameFilter() {
			public boolean accept(File dir, String name) {
				return name.toLowerCase().endsWith("." + format);
			}
		});
   
		System.out.println(title);
		fd.setFile(title + "." + format);
		fd.setVisible(true);
		
		String filename = fd.getFile();
		if (filename == null) {
			js.eval("showDownload();");
			this.resetProcessing(js);
			return;
		}
		if (filename.endsWith("." + format)) {
			filename = filename.substring(0, filename.length() - format.length() - 1);
		}
		
		String path = fd.getDirectory();
		String filenameBase = path;
		if (filenameBase.endsWith(File.separatorChar + "")) {
			filenameBase += filename;
		} else {
			filenameBase += File.pathSeparator + filename;
		}
		
		System.out.println(filenameBase);
		OnlineMp3Converter converter = null;
		try {
			converter = new OnlineMp3Converter(videoUrl, filenameBase);
		} catch (IOException e) {
			System.out.println(e.toString());
		}

		if (converter == null) {
			String msg = "Convert failed!";
			this.showMsg(js, msg);
			this.resetProcessing(js);
			return;
		}
		
		converter.addListener(new IObjectListener<Double>() {
			@Override
			public void onMessage(Double progress) {
				js.eval("showProgress('" + progress + "');");
			}
			
			@Override
			public void onError(String msg) {
				Convert2Mp3Applet.this.showMsg(js, msg);
			}
		});
		
		try {
			converter.extractStreams();
			String eval = "showResult('" + path.replaceAll("[\\\\]", "/") + "');";
			System.out.println(eval);
			js.eval(eval);
		} catch (IOException e) {
			String msg = "Error extract streams!";
			this.showMsg(js, msg);
			this.resetProcessing(js);
			System.out.println(e.toString());
		} finally {
			converter.destroy();
		}
	}
	
	private void showMsg(JSObject js, String msg) {
		js.eval("showMsg('" + msg + "');");
	}
	
	private void resetProcessing(JSObject js) {
		js.eval("resetProcessing();");
	}
	
	private HashMap<String, String> getVideoEmbedAAC(ArrayList<HashMap<String, String>> list) {
		HashMap<String, HashMap<String, String>> mapResult = new HashMap<String, HashMap<String, String>>();
		for (HashMap<String, String> map : list) {
			String formatCode = map.get("formatCode");

			if ("35".equals(formatCode) || "34".equals(formatCode)) {
				mapResult.put(formatCode, map);
			}
		}
		
		if (mapResult.containsKey("35")) {
			return mapResult.get("35");
		}
		
		return mapResult.get("34");
	}
	
	private HashMap<String, String> getVideoEmbedMP3(ArrayList<HashMap<String, String>> list) {
		HashMap<String, HashMap<String, String>> mapResult = new HashMap<String, HashMap<String, String>>();
		for (HashMap<String, String> map : list) {
			String formatCode = map.get("formatCode");

			if ("6".equals(formatCode) || "5".equals(formatCode)) {
				mapResult.put(formatCode, map);
			}
		}
		
		if (mapResult.containsKey("6")) {
			return mapResult.get("6");
		}
		
		return mapResult.get("5");
	}
	
	private ArrayList<HashMap<String, String>> parse(String pageUrl) {
		ArrayList<HashMap<String, String>> listResult = this.parseByAPI(pageUrl);
		if (listResult.size() < 1) {
			listResult = this.parseByHtml(pageUrl);
		}
		
		return listResult;
	}
	
	private ArrayList<HashMap<String, String>> parseByAPI(String pageUrl) {
		ArrayList<HashMap<String, String>> listResult = new ArrayList<HashMap<String,String>>();
		String playbackToken;
		try {
			playbackToken = this.downloadAPISource(pageUrl);
		} catch (Exception e) {
			return listResult;
		}
		
		if (playbackToken.startsWith("account_playback_token=")) {
			playbackToken = playbackToken.substring("account_playback_token=".length() + 1);
		}
		Map<String, String> map = this.parseQueryString(playbackToken);
		if ("fail".equalsIgnoreCase((String) map.get("status"))) {
//			String reason = (String) map.get("reason");
//			reason = URLDecoder.decode(reason, APPCHARSET);
			return listResult;
		}

		String title = (String) map.get("title");
		try {
			title = URLDecoder.decode(title, APPCHARSET);
		} catch (UnsupportedEncodingException e1) {
			e1.printStackTrace();
		}
		
		title = title.replaceAll("&gt;", "&").replaceAll("&amp;", "&")
				.replaceAll("&lt;", "<").replaceAll("&gt;", ">")
				.replaceAll("&nbsp;", " ").replaceAll("'", "")
				.replaceAll("&quot;", "\"").replaceAll("<br>", "\n");

		title = title.replaceAll("[:*?%\"<>|]", "");
		title = title.replaceAll("[\\/]", "-");
		
		String availableFormats = (String) map.get("url_encoded_fmt_stream_map");

		String argument = "url%3D";
		String endOfQueryString = "%26";

		if (!"".equals(availableFormats)) {
			String[] urlList = availableFormats.split(argument);
			for (String url : urlList) {
				String u = url.trim();
				if (!"".equals(u)) {
					int index = u.indexOf(endOfQueryString);
					u = u.substring(0, index);

					try {
						u = URLDecoder.decode(u, APPCHARSET);
						u = URLDecoder.decode(u, APPCHARSET);
					} catch (UnsupportedEncodingException e) {
						return listResult;
					}
					
					URL uri;
					try {
						uri = new URL(u);
					} catch (MalformedURLException e) {
						return listResult;
					}
					
					Map<String, String> queryString = this.parseQueryString(uri.getQuery());
					String itag = (String) queryString.get("itag");
					if (itag == null) {
						continue;
					}
					
					byte formatCode = Byte.parseByte(itag);
					String format = this.getVideoFormat(formatCode);
					String quality = this.getVideoQuality(formatCode);
					String fileExt = this.getFileExt(formatCode);
					String videoUrl = u;
				
					HashMap<String, String> item = new HashMap<String, String>();
					item.put("formatCode", formatCode + "");
					item.put("format", format);
					item.put("quality", quality);
					item.put("url", videoUrl);
					item.put("title", title);
					item.put("ext", fileExt);
					
					listResult.add(item);
				}
			}
		}

		return listResult;
	}
	
	private ArrayList<HashMap<String, String>> parseByHtml(String pageUrl) {
		String htmlSource = this.downloadHtmlSource(pageUrl);
		if ("".equals(htmlSource)) {
			return new ArrayList<HashMap<String,String>>();
		}
		
		String title = this.parseVideoTitle(htmlSource);
		
		ArrayList<HashMap<String, String>> listResult = this.parseByJsonDownloadUrl(title, htmlSource);
		if (listResult.size() < 1) {
			listResult = this.parseByMapDownloadUrl(title, htmlSource);
		}
		
		return listResult;
	}
	
	private String parseVideoTitle(String htmlSource) {
		String videoTitlePattern = "<meta name=\"title\" content=\"(.*)\">";
		Pattern pattern = Pattern.compile(videoTitlePattern, 2);
		Matcher matcher = pattern.matcher(htmlSource);

		if (matcher.find()) {
			String title = matcher.group(1);

			title = title.replaceAll("&gt;", "&").replaceAll("&amp;", "&")
					.replaceAll("&lt;", "<").replaceAll("&gt;", ">")
					.replaceAll("&nbsp;", " ").replaceAll("'", "")
					.replaceAll("&quot;", "\"").replaceAll("<br>", "\n");

			title = title.replaceAll("[:*?%\"<>|]", "");
			title = title.replaceAll("[\\/]", "-");

			return title;
		}
		
		return "";
	}
	
	private ArrayList<HashMap<String, String>> parseByJsonDownloadUrl(String title, String htmlSource) {
		ArrayList<HashMap<String, String>> listResult = new ArrayList<HashMap<String,String>>();
		
		String startConfig = "yt.playerConfig = ";
		int playerConfigIndex = htmlSource.indexOf(startConfig);

		if (playerConfigIndex >= 0) {
			int endOfJsonIndex = htmlSource.indexOf("};",
					playerConfigIndex + startConfig.length());
			String signature = htmlSource.substring(playerConfigIndex
					+ startConfig.length(), endOfJsonIndex + "};".length() - 1);
			String code = "\"url_encoded_fmt_stream_map\":";
			int index = signature.indexOf(code) + code.length();
			String availableFormats = signature.substring(index).trim();
			index = availableFormats.indexOf("\",");
			availableFormats = availableFormats.substring(1, index);

			String argument = "url=";
			String endOfQueryString = "\\u0026";

			if (!"".equals(availableFormats)) {
				String[] urlList = availableFormats.split(argument);
				for (String url : urlList) {
					String u = url.trim();
					if (!"".equals(u)) {
						index = u.indexOf(endOfQueryString);
						u = u.substring(0, index);

						try {
							u = URLDecoder.decode(u, APPCHARSET);
							u = URLDecoder.decode(u, APPCHARSET);
						} catch (UnsupportedEncodingException e) {
							return listResult;
						}
						
						URL uri;
						try {
							uri = new URL(u);
						} catch (MalformedURLException e) {
							return listResult;
						}
						
						Map<String, String> queryString = parseQueryString(uri.getQuery());
						String itag = (String) queryString.get("itag");
						if (itag == null) {
							continue;
						}
						byte formatCode = Byte.parseByte(itag);

						String format = this.getVideoFormat(formatCode);
						String quality = this.getVideoQuality(formatCode);
						String fileExt = this.getFileExt(formatCode);
						String videoUrl = u;
					
						HashMap<String, String> item = new HashMap<String, String>();
						item.put("formatCode", formatCode + "");
						item.put("format", format);
						item.put("quality", quality);
						item.put("url", videoUrl);
						item.put("title", title);
						item.put("ext", fileExt);
						
						listResult.add(item);
					}
				}
			}
		}
		
		return listResult;
	}

	private ArrayList<HashMap<String, String>> parseByMapDownloadUrl(String title, String htmlSource) {
		ArrayList<HashMap<String, String>> listResult = new ArrayList<HashMap<String,String>>();
		
		String startStreamMap = "url_encoded_fmt_stream_map=";
		int startStreamMapIndex = htmlSource.indexOf(startStreamMap);

		if (startStreamMapIndex >= 0) {
			int endOfJsonIndex = htmlSource.indexOf("&amp;",
					startStreamMapIndex + startStreamMap.length());
			String availableFormats = htmlSource.substring(
					startStreamMapIndex + startStreamMap.length(),
					endOfJsonIndex + "&amp;".length());
			String argument = "url%3D";
			String endOfQueryString = "%26";

			if (!"".equals(availableFormats)) {
				String[] urlList = availableFormats.split(argument);
				for (String url : urlList) {
					String u = url.trim();
					if (!"".equals(u)) {
						int index = u.indexOf(endOfQueryString);
						u = u.substring(0, index);

						try {
							u = URLDecoder.decode(u, APPCHARSET);
							u = URLDecoder.decode(u, APPCHARSET);
						} catch (UnsupportedEncodingException e) {
							return listResult;
						}
						
						URL uri;
						try {
							uri = new URL(u);
						} catch (MalformedURLException e) {
							return listResult;
						}
						
						Map<String, String> queryString = this.parseQueryString(uri.getQuery());
						String itag = (String) queryString.get("itag");
						if (itag == null) {
							continue;
						}
						byte formatCode = Byte.parseByte(itag);
						
						String format = this.getVideoFormat(formatCode);
						String quality = this.getVideoQuality(formatCode);
						String fileExt = this.getFileExt(formatCode);
						String videoUrl = u;
					
						HashMap<String, String> item = new HashMap<String, String>();
						item.put("formatCode", formatCode + "");
						item.put("format", format);
						item.put("quality", quality);
						item.put("url", videoUrl);
						item.put("title", title);
						item.put("ext", fileExt);
						
						listResult.add(item);
					}
				}
			}
		}
		
		return listResult;
	}
	
	private String downloadHtmlSource(String pageUrl) {
		HttpURLConnection conn = null;
		InputStreamReader isr = null;
		try {
			URL url = new URL(pageUrl);
			conn = (HttpURLConnection) url.openConnection();
			conn.addRequestProperty("User-Agent", super.getParameter("ua"));
			isr = new InputStreamReader(conn.getInputStream(), APPCHARSET);
			StringBuilder sb = new StringBuilder();
			char[] buffer = new char[1024];
			while (true) {
				int len = isr.read(buffer, 0, buffer.length);
				if (len <= 0) {
					break;
				}
				sb.append(buffer, 0, len);
			}
			String htmlSource = sb.toString();
			return htmlSource;
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			if (isr != null) {
				try {
					isr.close();
				} catch (IOException e) {
				}
			}
			if (conn != null) {
				conn.disconnect();
			}
		}
		
		return "";
	}
	
	private String downloadAPISource(String pageUrl) throws RuntimeException, IOException {
		HttpURLConnection conn = null;
		InputStreamReader isr = null;
		try {
			int index = pageUrl.indexOf("?");
			if (index < 0) {
				throw new RuntimeException("Invalid YouTube Url");
			}
			String queryString = pageUrl.substring(index + 1);
			Map<String, String> map = this.parseQueryString(queryString);
			String videoId = (String) map.get("v");
			if (videoId == null || "".equals(videoId)) {
				throw new RuntimeException("Invalid YouTube Url");
			}

			String apiUrl = "http://www.youtube.com/get_video_info?video_id="
					+ videoId
					+ "&el=embedded&ps=default&autoplay=1&eurl=http%3A%2F%2Fwww%2Eyoutube%2Ecom&hl=en_US";

			URL url = new URL(apiUrl);
			conn = (HttpURLConnection) url.openConnection();
			conn.addRequestProperty("User-Agent", super.getParameter("ua"));
			isr = new InputStreamReader(conn.getInputStream(), APPCHARSET);

			StringBuilder sb = new StringBuilder();
			char[] buffer = new char[1024];
			while (true) {
				int len = isr.read(buffer, 0, buffer.length);
				if (len <= 0) {
					break;
				}
				sb.append(buffer, 0, len);
			}
			String source = sb.toString();
			return source;
		} finally {
			if (isr != null) {
				try {
					isr.close();
				} catch (IOException localIOException) {
				}
			}
			if (conn != null) {
				conn.disconnect();
			}
		}
	}
	
	private String getFileExt(byte formatCode) {
		switch (formatCode) {
		case 43:
		case 44:
		case 45:
		case 46:
			return ".webm";
		case 18:
		case 22:
		case 37:
		case 38:
		case 82:
		case 84:
			return ".mp4";
		case 5:
		case 6:
		case 34:
		case 35:
			return ".flv";
		case 13:
		case 17:
			return ".3pg";
		}

		return "";
	}

	private String getVideoQuality(byte formatCode) {
		switch (formatCode) {
		case 43:
			return "WebM Low Quality (360p)";
		case 44:
			return "WebM Standard Quality (480p)";
		case 45:
			return "WebM High Quality (720p)";
		case 46:
			return "WebM High Definition (1080p)";
		case 38:
			return "MP4 High Quality (4096p)";
		case 37:
			return "MP4 High Definition (1080p)";
		case 22:
			return "MP4 High Quality (720p)";
		case 82:
			return "MP4 3D Standard Quality (360p)";
		case 84:
			return "MP4 3D High Quality (720p)";
		case 35:
			return "FLV High Definition (480p)";
		case 34:
			return "FLV Standard Quality (360p)";
		case 18:
			return "MP4 Standard Quality (360p)";
		case 6:
			return "FLV Low Quality (270p)";
		case 5:
			return "FLV Low Quality (240p)";
		case 13:
		case 17:
			return "MP4 Mobile Video";
		}

		return "";
	}
	
	private String getVideoFormat(byte formatCode) {
		switch (formatCode) {
		case 5:
		case 6:
		case 34:
		case 35:
			return VideoType_Flash;
		case 18:
		case 22:
		case 37:
		case 38:
		case 82:
		case 84:
			return VideoType_MP4;
		case 13:
		case 17:
			return VideoType_Mobile;
		case 43:
		case 45:
		case 46:
			return VideoType_WebM;
		}

		return VideoType_Unknown;
	}
	
	private Map<String, String> parseQueryString(String queryString) {
		Map<String, String> map = new HashMap<String, String>();
		if (queryString == null || "".equals(queryString)) {
			return map;
		}

		int index = queryString.indexOf('?') + 1;
		if (index >= 0) {
			queryString = queryString.substring(index);
		}

		StringBuilder sbValue = new StringBuilder();
		String key = "";
		for (index = 0; index < queryString.length(); index++) {
			char ch = queryString.charAt(index);
			if (ch == '=') {
				if (sbValue.length() > 0) {
					key = sbValue.toString();
					sbValue = new StringBuilder();
				}
			} else if (ch == '&') {
				if (!"".equals(key)) {
					map.put(key, sbValue.toString());
				}
				sbValue = new StringBuilder();
			} else {
				sbValue.append(ch);
			}
		}

		if (sbValue.length() > 0) {
			map.put(key, sbValue.toString());
		}

		return map;
	}
}
